//
// Created by LiuYou on 2021/8/20.
//

#include <sys/epoll.h>
#include <fcntl.h>
#include <cstring>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <sys/mman.h>
#include <sys/uio.h>
#include <cstdarg>
#include <cerrno>
#include <map>


#include "HttpConnect2.hpp"
#include "DbConnectionPool.hpp"

// 服务端资源页的路径，将其和HTTP请求中解析的m_url拼接形成资源页的位置(DOC_ROOT + url)
// 网站根目录，文件夹内存放请求的资源和跳转的html文件
//static const char* DOC_ROOT = "/var/www/html";
static const char* DOC_ROOT = "/home/liuyou/CLionProjects/HighPerformanceLinuxServerProgramming_Linux2/resource";


static const char* ERROR_500_TITLE = "Internal Error";

static const char* ERROR_500_FORM = "There was an unusual problem serving the requested file.\n";


static const char* ERROR_400_TITLE = "Bad Request";

static const char* ERROR_400_FORM = "Your request has bad syntax or is inherently impossible to satisfy.\n";


static const char* ERROR_404_TITLE = "Not Found";

static const char* ERROR_404_FORM = "The requested file was not found on this server.\n";


static const char* ERROR_403_TITLE = "Forbidden";

static const char* ERROR_403_FORM = "Forbidden";


static const char* OK_200_TITLE = "OK";

//将表中的用户名和密码放入map
static std::map<std::string, std::string> users;
Locker m_lock;


int HttpConnect::epollFd = -1;
int HttpConnect::userCount = 0;


////////////////////////////////////////////////////////////////////////////////////

/*static*/ int setFdNonblocking(int fd) {
    int oldOption = fcntl( fd, F_GETFL );
    // 疑惑解开, 将文件描述符设置为非阻塞, 使用的是宏 O_NONBLOCK。
    int newOption = oldOption | O_NONBLOCK;
    fcntl( fd, F_SETFL, newOption );
    return oldOption;
}

/*static*/ void addFdToEpoll(int epollFd, int fd, bool oneShot) {
    epoll_event epollEvent{};
    epollEvent.data.fd = fd;
    epollEvent.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    if (oneShot) {
        epollEvent.events |= EPOLLONESHOT;
    }
    setFdNonblocking( fd );
    epoll_ctl( epollFd, EPOLL_CTL_ADD, fd, &epollEvent );
}


/*static*/ void removeFd(int epollFd, int fd) {
    epoll_ctl( epollFd, EPOLL_CTL_DEL, fd, nullptr );
    close( fd );
}

/*static*/ void modFd(int epollFd, int fd, EPOLL_EVENTS events) {
    epoll_event epollEvent{};
    epollEvent.data.fd = fd;
    epollEvent.events = events | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    epoll_ctl( epollFd, EPOLL_CTL_MOD, fd, &epollEvent );
}




///////////////////////////////////////////////////////////////////////////


//HttpConnect::HttpConnect() = default;
//
//HttpConnect::~HttpConnect() = default;


/**
 * @brief 重载init函数进行些连接前的初始化操作。
 */
void HttpConnect::init() {
    // 主状态机的初始状态。即请求报文的请求行, 请求行状态。
    // 这里把客户端(浏览器)的请求报文(请求首部+报文主体)分为了,
    // 请求行
    // 请求头部
    // 报文主体
    checkState = CheckState::CHECK_STATE_REQUEST_LINE;
    linger = false;

    // 默认 http方法为 GET, 同时也只支持 GET方法。
    method = Method::GET;
    url = nullptr;
    version = nullptr;
    contentLength = 0;
    host = nullptr;
    startLine = 0;
    checkedIdx = 0;
    readIdx = 0;
    writeIdx = 0;


    // 新添加的几个字段的赋值操作。
    mysql = nullptr;
    cgi = 0;
    m_string = nullptr;
    bytesToSend = 0;
    bytesHaveSend = 0;


    memset( readBuf, '\0', READ_BUFFER_SIZE );
    memset( writeBuf, '\0', WRITE_BUFFER_SIZE );
    memset( realFile, '\0', FILE_NAME_LEN );
}


/**
 * @brief 初始化连接
 * @param connectionFd
 * @param address
 */
void HttpConnect::init(int connectionFd, const sockaddr_in& address) {
    // connectFd是http连接对应的描述符用于接收http请求和http回答
    connectFd = connectionFd;
    // 客户端地址
    remoteAddress = address;

    /** 如下两行是为了避免 TIME_WAIT状态, 仅用于调试, 实际使用时应该去掉。*/
    int reuse = 1;
    setsockopt( connectFd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ));

    // 将这个文件描述符加入到epoll监听队列中。
    addFdToEpoll( epollFd, connectFd, true );
    // 多了一个http用户
    ++userCount;

    init();
}


/**
 * @brief 关闭连接，从事件表中移除描述符。
 * @param realClose
 */
void HttpConnect::closeConnection(bool realClose) {
    if (realClose && (connectFd != -1)) {
        removeFd( epollFd, connectFd );
        connectFd = -1;
        /** 关闭一个连接时, 将客户总量减 1。*/
        --userCount;
    }
}


/**
 * @brief 有线程池中的工作线程(即创建出来的8个线程之一)调用, 这是处理HTTP请求的入口函数。
 * <p>
 * 各子线程通过process函数对任务进行处理，调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务。
 *
 */
void HttpConnect::process() {
    // 调用 processRead()函数 对浏览器发来的 http请求报文进行解析。
    HttpCode readRet = processRead();
    // NO_REQUEST，表示请求不完整，需要继续接收请求数据
    if (readRet == HttpCode::NO_REQUEST) {
        // 修改(更新)当前的connectFd之前在epoll中注册的监听读事件, 因为请求不完整, 还需要继续接受请求数据。
        modFd( epollFd, connectFd, EPOLLIN );
        return;
    }


    // 调用 processWrite()函数 向浏览器发送响应报文。
    // 根据 主状态机 processRead()处理后返回的http状态码(do_request的返回的http状态码)，
    // 服务器子线程调用process_write向m_write_buf中写入响应报文。
    bool writeRet = processWrite( readRet );
    if (!writeRet) {
        closeConnection();
    }
    // 修改(更新)当前的connectFd之前在epoll中注册的监听读事件, 因为当前的connectFd已经向浏览器发送了响应报文,
    // 需要更新connectFd在epoll中注册的读事件为写事件。
    modFd( epollFd, connectFd, EPOLLOUT );
}


/** 非阻塞读操作。
* <p>
* 循环读取客户数据, 直到无数据可读或者对方关闭连接。
* <p>
* read()方法, 读取浏览器端发送来的请求报文, 直到无数据可读或对方关闭连接,
* 读取到m_read_buffer中, 并更新m_read_idx。*/
bool HttpConnect::read() {
    // idx: 序号，索引。
    // 如果读取索引大于或者等于了读取缓冲区的大小, 那这个函数返回错误。
    if (readIdx >= READ_BUFFER_SIZE) {
        return false;
    }

    // 循环读取客户端发来的数据, 直到无数据苦读或者对方关闭连接。
    // 读取到m_read_buffer中, 并更新m_read_idx。
    while (true) {
        // 从套接字接收数据，存储在m_read_buf缓冲区
        ssize_t rcvByteNum = recv( connectFd, readBuf + readIdx, READ_BUFFER_SIZE - readIdx, 0 );
        if (rcvByteNum == -1) {
            // 非阻塞ET模式下(即LT模式下)，需要一次性将数据读完
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                break;
            }
            return false;
        } else if (rcvByteNum == 0) {
            return false;
        }

        // 更新m_read_idx的读取字节数
        readIdx += rcvByteNum;
    }
    return true;
}


/**
 * @brief 写 HTTP 响应(response)。
 * <p>
 * 将资源页文件发送给客户端
 * <p>
 * 服务器子线程调用process_write完成响应报文，随后注册epollout事件。
 * 服务器主线程检测写事件，并调用http_conn::write函数将响应报文发送给浏览器端。
 * <p>
该函数具体逻辑如下：

在生成响应报文时初始化byte_to_send，包括头部信息和文件数据大小。通过writev函数循环发送响应报文数据，
 根据返回值更新byte_have_send和iovec结构体的指针和长度，并判断响应报文整体是否发送成功。

若writev单次发送成功，更新byte_to_send和byte_have_send的大小，若响应报文整体发送成功,则取消mmap映射,并判断是否是长连接.

长连接重置http类实例，注册读事件，不关闭连接，

短连接直接关闭连接

若writev单次发送不成功，判断是否是写缓冲区满了。

若不是因为缓冲区满了而失败，取消mmap映射，关闭连接

若eagain则满了，更新iovec结构体的指针和长度，并注册写事件，等待下一次写事件触发（当写缓冲区从不可写变为可写，触发epollout），
 因此在此期间无法立即接收到同一用户的下一请求，但可以保证连接的完整性。
 *
 *
 * <p>
 * 原书这里设计有缺陷。
 * @return
 */
bool HttpConnect::write() {
    int newAdd = 0;

    // 若要发送的数据长度为0
    // 表示响应报文为空，一般不会出现这种情况
    if (bytesToSend == 0) {
        modFd( epollFd, connectFd, EPOLLIN );
        init();
        return true;
    }

//    ssize_t bytesHaveSend = 0;
    while (true) {
        // 将响应报文的状态行、消息头、空行和响应正文发送给浏览器端。
        //
        // 集中写，connectFd是http连接对应的描述符，ioVector是iovec结构体数组表示内存块地址，
        // ioVectorCount是数组的长度即多少个内存块将一次集中写到connectFd
        ssize_t writeToFdBytesNum = writev( connectFd, ioVector, ioVectorCount );
        // 集中写成功。正常发送，writeToFdBytesNum为发送的字节数, 即写入到 connectFd中的字节数。
        if (writeToFdBytesNum > 0) {
            // 更新已发送字节
            bytesHaveSend += writeToFdBytesNum;
            // 偏移文件iovec的指针
            newAdd = bytesHaveSend - writeIdx;
        }
        if (writeToFdBytesNum <= -1) {
            // 判断缓冲区是否满了
            if (errno == EAGAIN) {
                // 第一个iovec头部信息的数据已发送完，发送第二个iovec数据
                if (bytesHaveSend >= ioVector[0].iov_len) {
                    // 不再继续发送头部信息
                    ioVector[0].iov_len = 0;
                    ioVector[1].iov_base = fileAddress + newAdd;
                    ioVector[1].iov_len = bytesToSend;
                }
                    // 继续发送第一个iovec头部信息的数据
                else {
                    ioVector[0].iov_base = writeBuf + bytesToSend;
                    ioVector[0].iov_len = ioVector[0].iov_len - bytesHaveSend;
                }
                // 重新注册写事件
                modFd( epollFd, connectFd, EPOLLOUT );
                return true;
            }
            //如果发送失败，但不是缓冲区问题，取消映射
            unmap();
            return false;
        }

        //<editor-fold desc="书上的代码">
        // 集中写失败
//        if (writeToFdBytesNum <= -1) {
//            // 判断缓冲区是否满了
//            /* 如果TCP写缓冲没有空间, 则等待下一轮EPOLLOUT事件。虽然在此期间, 服务器无法立刻接受到同一客户的下一个请求,
//             * 但这可以保证连接的完整性。*/
//            if (errno == EAGAIN) {
//                //重置EPOLLONESHOT事件,注册可写事件表示若connectFd没有写失败则关闭连接
//                modFd( epollFd, connectFd, EPOLLOUT );
//                return true;
//            }
//            // 如果发送失败，但不是缓冲区问题，取消映射
//            //
//            // 解除内存映射, 回收内存。
//            unmap();
//            return false;
//        }
        //</editor-fold>

        // 更新要发送数据和已发送数据的字节数
        //
        // 待发送数据
        bytesToSend -= writeToFdBytesNum;
        // 已发送数据
        bytesHaveSend += writeToFdBytesNum;

        // 判断条件，数据已全部发送完
        if (bytesToSend <= 0) {
            /* 发送HTTP响应成功, 根据HTTP请求中的Connection字段决定是否李隆基关闭连接。*/
            // 该资源页已经发送完毕该解除映射
            unmap();

            // 如果浏览器的请求为长连接
            // 若要保持该http连接
            if (linger) {
                // 初始化http连接
                init();
                modFd( epollFd, connectFd, EPOLLIN );
                return true;
            } else {
                modFd( epollFd, connectFd, EPOLLIN );
                return false;
            }
        }
    }
}


/**
 * @brief m_start_line是行在buffer中的起始位置，将该位置后面的数据赋给text,
 * 此时从状态机已提前将一行的末尾字符 \r\n 变为 \0\0，所以text可以直接取出完整的行进行解析。
 * @return
 */
char* HttpConnect::getLine() {
    return readBuf + startLine;
}


/**
 * @brief 主状态机。
 * <p>
 * <p>
 * process_read通过while循环，将主从状态机进行封装，对报文的每一行进行循环处理。
 * <p>
 * 判断条件
 * <p>
 *   主状态机转移到CHECK_STATE_CONTENT，该条件涉及解析消息体
 * <p>
 *   从状态机转移到LINE_OK，该条件涉及解析请求行和请求头部
 * <p>
 *   两者为或关系，当条件为真则继续循环，否则退出
 * <p>
 * 循环体
 * <p>
 *   从状态机读取数据
 * <p>
 *   调用get_line函数，通过m_start_line将从状态机读取数据间接赋给text
 * <p>
 *   主状态机解析text
 * <p>
 * <p>
 * <p>
 * <p>
 * 主状态机初始状态是CHECK_STATE_REQUEST_LINE，通过调用从状态机来驱动主状态机，
 * 在主状态机进行解析前，从状态机已经将每一行的末尾 \r\n 符号改为 \0\0 ，以便于主状态机直接取出对应字符串进行处理。
 * <p>
 * CHECK_STATE_REQUEST_LINE
 *
 * 主状态机的初始状态，调用parse_request_line函数解析请求行
 * 解析函数从m_read_buf中解析HTTP请求行，获得请求方法、目标URL及HTTP版本号
 * 解析完成后主状态机的状态变为CHECK_STATE_HEADER
 * <p>
 * 解析完请求行后，主状态机继续分析请求头。在报文中，请求头和空行的处理使用的同一个函数，
 * 这里通过判断当前的text首位是不是\0字符，若是，则表示当前处理的是空行，若不是，则表示当前处理的是请求头。
 * <p>
 * CHECK_STATE_HEADER
 *
 * 调用parse_headers函数解析请求头部信息
 *
 * 判断是空行还是请求头，若是空行，进而判断content-length是否为0，如果不是0，表明是POST请求，则状态转移到CHECK_STATE_CONTENT，否则说明是GET请求，则报文解析结束。
 *
 * 若解析的是请求头部字段，则主要分析connection字段，content-length字段，其他字段可以直接跳过，各位也可以根据需求继续分析。
 *
 * connection字段判断是keep-alive还是close，决定是长连接还是短连接
 *
 * content-length字段，这里用于读取post请求的消息体长度
 * <p>
 * CHECK_STATE_CONTENT
 *
 * 仅用于解析POST请求，调用parse_content函数解析消息体
 *
 * 用于保存post请求消息体，为后面的登录和注册做准备
 *
 * @return processRead()的返回值是对请求的文件分析后的结果(对http请求报文解析后的结果)，
 * 返回值可能是语法错误导致的BAD_REQUEST，也可能是do_request的返回结果。
 * 该函数将网站根目录和url文件拼接，然后通过stat判断该文件属性。另外，为了提高访问速度，通过mmap进行映射，将普通文件映射到内存逻辑地址。
 */
HttpCode HttpConnect::processRead() {
    // 初始化从状态机状态、HTTP请求解析结果
    LineStatus lineStatus = LineStatus::LINE_OK;
    HttpCode httpCode = HttpCode::NO_REQUEST;
    char* text = nullptr;


    while (
        /*
         * 在GET请求报文中，每一行都是\r\n作为结束，所以对报文进行拆解时，仅用从状态机的状态line_status=parse_line())==LINE_OK语句即可。
         * 但，在POST请求报文中，消息体的末尾没有任何字符，所以不能使用从状态机的状态，这里转而使用主状态机的状态作为循环入口条件。
         * 那后面的&& line_status==LINE_OK又是为什么？
         * 解析完消息体后，报文的完整解析就完成了，但此时主状态机的状态还是CHECK_STATE_CONTENT，也就是说，符合循环入口条件，还会再次进入循环，这并不是我们所希望的。
         * 为此，增加了该语句，并在完成消息体解析后，将line_status变量更改为LINE_OPEN，此时可以跳出循环，完成报文解析任务。
         */
            ((/*主状态机*/checkState == CheckState::CHECK_STATE_CONTENT) && (/*从状态机*/lineStatus == LineStatus::LINE_OK)) ||
            ((lineStatus = /*parse_line为从状态机的具体实现.*/parseLine()) == LineStatus::LINE_OK)) {
        // 通过调用 getLine(), text可以直接取出完整的行进行解析
        text = getLine();

        // m_start_line是每一个数据行在m_read_buf中的起始位置
        // m_checked_idx表示从状态机在m_read_buf中读取的位置
        startLine = checkedIdx;

        printf( "got 1 http line: %s\n", text );
//        printf( "得到 1 http 行: %s\n", text );

        // 主状态机的三种状态转移逻辑
        switch (checkState) {
            // 解析http请求报文的报文头部的请求行。
            case CheckState::CHECK_STATE_REQUEST_LINE: {
                // 解析请求行。
                httpCode = parseRequestLine( text );
                if (httpCode == HttpCode::BAD_REQUEST) {
                    return HttpCode::BAD_REQUEST;
                }
                break;
            }
                // 解析http请求报文的报文头部。
            case CheckState::CHECK_STATE_HEADER: {
                // 解析请求报文的请求头部。
                httpCode = parseHeaders( text );
                if (httpCode == HttpCode::BAD_REQUEST) {
                    return HttpCode::BAD_REQUEST;
                }
                    // 完整解析GET请求后，跳转到报文响应函数
                else if (httpCode == HttpCode::GET_REQUEST) {
                    return doRequest();
                }
                break;
            }
                // 解析http请求报文的报文主体。
            case CheckState::CHECK_STATE_CONTENT: {
                // 解析报文主体。
                // 注意, 这个项目中http报文主体仅仅是 登录名和密码。
                httpCode = parseContent( text );
                // 完整解析POST请求后，跳转到报文响应函数
                if (httpCode == HttpCode::GET_REQUEST) {
                    return doRequest();
                }
                // 解析完消息体即完成报文解析，避免再次进入循环，更新line_status
                // 这句代码非常的重要。
                lineStatus = LineStatus::LINE_OPEN;
                break;
            }
            default: {
                return HttpCode::INTERNAL_ERROR;
            }
        }
    }

    return HttpCode::NO_REQUEST;
}


/**
 * @brief 从状态机。用于解析出一行内容。
 * <p>
 * 从状态机负责读取报文的一行，主状态机负责对该行数据进行解析，主状态机内部调用从状态机，从状态机驱动主状态机。
 * <p>
 * 在HTTP报文中，每一行的数据由 \r\n 作为结束字符，空行则是仅仅是字符 \r\n。
 * 因此，可以通过查找 \r\n 将报文拆解成单独的行进行解析，项目中便是利用了这一点。
 * <p>
 * 从状态机负责读取buffer中的数据，将每行数据末尾的 \r\n 置为 \0\0，并更新从状态机在buffer中读取的位置m_checked_idx，
 * 以此来驱动主状态机解析。
 * <p>
 * <p>
 * 1. 从状态机从m_read_buf中逐字节读取，判断当前字节是否为\r, 如果接下来的字符是\n，将\r\n修改成\0\0，
 * 将m_checked_idx指向下一行的开头，则返回LINE_OK
 * <p>
 * 2. 接下来达到了buffer末尾，表示buffer还需要继续接收，返回LINE_OPEN. 否则，表示语法错误，返回LINE_BAD
 * <p>
 * 3. 当前字节不是\r，判断是否是\n（一般是上次读取到\r就到了buffer末尾，没有接收完整，再次接收时会出现这种情况）,
 * 如果前一个字符是\r，则将\r\n修改成\0\0，将m_checked_idx指向下一行的开头，则返回LINE_OK
 * <p>
 * 4. 当前字节既不是\r，也不是\n, 表示接收不完整，需要继续接收，返回LINE_OPEN
 * @return
 */
LineStatus HttpConnect::parseLine() {
    char temp;
    // checked_index指向buffer（应用程序读缓冲区）中当前正在分析的字节，read_index指向buffer中客户数据尾部的下一个字节;
    // buffer中的第0 ~ checked_index字节都已分析完毕, 第checked_index ~ (read_index - 1)字节由下面的循环挨个分析
    for (; checkedIdx < readIdx; ++checkedIdx) {
        //获得当前要分析的字节, temp为将要分析的字节
        temp = readBuf[checkedIdx];
        //如果\r恰巧是目前buffer中最后一个已经被读入的客户数据，那么这次分析没有读取到完整的行，
        //返回LINE_OPEN表示还需要继续读取客户数据来进一步分析
        if (temp == '\r') {
            if ((checkedIdx + 1) == readIdx) {
                return LineStatus::LINE_OPEN;
            }
                //如果下一个字符是\n,说明读到一个完整的行
            else if (readBuf[checkedIdx + 1] == '\n') {
                // 终于找到错的地方了。
                readBuf[checkedIdx++] = '\0';
                readBuf[checkedIdx++] = '\0';
                return LineStatus::LINE_OK;
            }
            //否则，说明存在语法问题
            return LineStatus::LINE_BAD;
            //如果当前字符是\n，说明也可能读到一个完整的行
            //not necessary
        } else if (temp == '\n') {
            if ((checkedIdx > 1) && (readBuf[checkedIdx - 1] == '\r')) {
                readBuf[checkedIdx - 1] = '\0';
                readBuf[checkedIdx++] = '\0';
                return LineStatus::LINE_OK;
            }
            return LineStatus::LINE_BAD;
        }
    }
    // 如果所有内容分析完毕都没有遇到\r字符，则返回LINE_OPEN，表示还需要继续读取客户数据才能进一步分析
    return LineStatus::LINE_OPEN;
}

/**
 * @brief 解析http请求行，获得请求方法，目标url及http版本号
 * @param text
 * @return
 */
HttpCode HttpConnect::parseRequestLine(char* text) {
    // strpbrk()函数检索两个字符串中首个相同字符的位置，其原型为：
    //    char *strpbrk( char *s1, char *s2)
    // 在HTTP报文中，请求行用来说明请求类型,要访问的资源以及所使用的HTTP版本，其中各个部分之间通过\t或空格分隔。
    // 请求行中最先含有空格和\t任一字符的位置并返回
    url = strpbrk( text, " \t" );
    //请求行中如果没有\t，则该HTTP请求有问题
    if (!url) {
        return HttpCode::BAD_REQUEST;
    }
    // 将该位置改为\0，用于将前面数据取出。
    // 这句非常的重要。
    *url++ = '\0';

    char* method = text;
    //函数声明：int strcasecmp (const char *s1, const char *s2);
    //函数说明：strcasecmp()用来比较参数s1 和s2 字符串，比较时会自动忽略大小写的差异。
    //返回值：若参数s1 和s2 字符串相同则返回0。s1 长度大于s2 长度则返回大于0 的值，s1 长度若小于s2 长度则返回小于0 的值。
    //
    // 取出数据，并通过与GET和POST比较，以确定请求方式。
    if (strcasecmp( method, "GET" ) == 0) {
        this->method = Method::GET;
    } else if (strcasecmp( method, "POST" ) == 0) {
        this->method = Method::POST;
        // 这个字段改为bool类型也可以。
        cgi = 1;
    } else {
        return HttpCode::BAD_REQUEST;
    }

    //strspn() 函数用来计算字符串 str 中连续有几个字符都属于字符串 accept，其原型为：
    //size_t strspn(const char *str, const char * accept);
    // m_url此时跳过了第一个空格或\t字符，但不知道之后是否还有
    // 将m_url向后偏移，通过查找，继续跳过空格和\t字符，指向请求资源的第一个字符
    url += strspn( url, " \t" );
    // 使用与判断请求方式的相同逻辑，判断HTTP版本号
    version = strpbrk( url, " \t" );
    if (!version) {
        return HttpCode::BAD_REQUEST;
    }
    // 重要
    *version++ = '\0';
    version += strspn( version, " \t" );
    // 仅支持HTTP/1.1
    if (strcasecmp( version, "HTTP/1.1" ) != 0) {
        return HttpCode::BAD_REQUEST;
    }

    // 对请求资源前7个字符进行判断
    // 这里主要是有些报文的请求资源中会带有http://，这里需要对这种情况进行单独处理
    if (strncasecmp( url, "http://", 7 ) == 0) {
        url += 7;
        //strchr() 用来查找某字符在字符串中首次出现的位置，其原型为：
        //char * strchr (const char *str, int c)
        url = strchr( url, '/' );
    }

    // 一般的不会带有上述两种符号，直接是单独的/或/后面带访问资源
    if (!url || url[0] != '/') {
        return HttpCode::BAD_REQUEST;
    }

    //当url为/时，显示欢迎界面
    if (strlen( url ) == 1) {
        strcat( url, "welcome.html" );
    }

    //HTTP请求行处理完毕，状态转移到头部字段的分析
    checkState = CheckState::CHECK_STATE_HEADER;
    return HttpCode::NO_REQUEST;
}


/**
 * @brief 解析http请求的一个头部信息。
 * @param text
 * @return
 */
HttpCode HttpConnect::parseHeaders(char* text) {
    // 空行，头部字段解析完毕
    // 判断是空行还是请求头
    if (text[0] == '\0') {
        /* 如果HTTP请求有消息体, 则还需要读取contentLength字节的消息体, 状态机转移到 CHECK_STATE_CONTENT状态。*/
        // 判断是空行还是请求头
        if (contentLength != 0) {
            // POST需要跳转到消息体处理状态
            checkState = CheckState::CHECK_STATE_CONTENT;
            return HttpCode::NO_REQUEST;
        }
        /* 否则说明我们已经得到了一个完整的HTTP请求。*/
        return HttpCode::GET_REQUEST;
    }
        /* 解析Connection头部字段。*/
    else if (strncasecmp( text, "Connection:", 11 ) == 0) {
        text += 11;
        //跳过空格和\t字符
        text += strspn( text, " \t" );
        if (strcasecmp( text, "keep-alive" ) == 0) {
            //如果是长连接，则将linger标志设置为true
            linger = true;
        }
    }
        /* 解析Content-Length头部字段。*/
    else if (strncasecmp( text, "Content-Length:", 15 ) == 0) {
        text += 15;
        text += strspn( text, " \t" );
        contentLength = atol( text );
    }
        /* 解析Host头部字段。*/
    else if (strncasecmp( text, "Host:", 5 ) == 0) {
        text += 5;
        text += strspn( text, " \t" );
        host = text;
    }
        /* 其它头部字段不解析不处理。*/
    else {
        printf( "Oops! unknown header %s\n", text );
    }
    return HttpCode::NO_REQUEST;
}


/**
 * @brief 我们没有真正解析http请求的消息体, 只是判断它是否被完整地读入了。
 * <p>
 * 消息体不解析，判断一下是否完整读入。
 * <p>
 * 现在不了, 现在要解析出来用户名和密码。
 * @param text
 * @return
 */
HttpCode HttpConnect::parseContent(char* text) {
    // 判断buffer中是否读取了消息体
    if (readIdx >= (contentLength + checkedIdx)) {
        text[contentLength] = '\0';

        // POST请求中最后为输入的用户名和密码
        // m_string字段为新增的字段。跟数据库交互有关。
        m_string = text;


        return HttpCode::GET_REQUEST;
    }
    return HttpCode::NO_REQUEST;
}


/**
 * @brief 当得到一个完整、正确的HTTP请求时, 我们就分析目标文件的属性。如果目标文件存在、对所有用户可读, 且不是目录,
 * 则使用 mmap() 将其映射到内存地址 fileAddress处, 并告诉调用者获取文件成功。
 * <p>
 * 用于获取资源页文件的状态。

process_read函数的返回值是对请求的文件分析后的结果，一部分是语法错误导致的BAD_REQUEST，一部分是do_request的返回结果.该函数将网站根目录和url文件拼接，然后通过stat判断该文件属性。另外，为了提高访问速度，通过mmap进行映射，将普通文件映射到内存逻辑地址。

为了更好的理解请求资源的访问流程，这里对各种各页面跳转机制进行简要介绍。其中，浏览器网址栏中的字符，即url，可以将其抽象成ip:port/xxx，xxx通过html文件的action属性进行设置。

m_url为请求报文中解析出的请求资源，以/开头，也就是/xxx，项目中解析后的m_url有8种情况。

/

GET请求，跳转到judge.html，即欢迎访问页面

/0

POST请求，跳转到register.html，即注册页面

/1

POST请求，跳转到log.html，即登录页面

/2CGISQL.cgi

POST请求，进行登录校验

验证成功跳转到welcome.html，即资源请求成功页面

验证失败跳转到logError.html，即登录失败页面

/3CGISQL.cgi

POST请求，进行注册校验

注册成功跳转到log.html，即登录页面

注册失败跳转到registerError.html，即注册失败页面

/5

POST请求，跳转到picture.html，即图片请求页面

/6

POST请求，跳转到video.html，即视频请求页面

/7

POST请求，跳转到fans.html，即关注页面

 * @return
 */
HttpCode HttpConnect::doRequest() {
    // 将初始化的m_real_file赋值为网站根目录
    strcpy( realFile, DOC_ROOT );
    size_t len = strlen( DOC_ROOT );

    // ...

    // 找到m_url中/的位置
    char* pos = strrchr( url, '/' );

    // 实现登录和注册校验
    // 处理cgi
    if (cgi == 1 &&
        (*(pos + 1) == '2' || *(pos + 1) == '3')) {
        //根据标志判断是登录检测还是注册检测
        char flag = url[1];

        char* m_url_real = (char*) malloc( sizeof( char ) * 200 );
        strcpy( m_url_real, "/" );
        strcat( m_url_real, url + 2 );
        strncpy( realFile + len, m_url_real, FILE_NAME_LEN - len - 1 );
        free( m_url_real );

        //将用户名和密码提取出来
        //user=123&password=123
        char name[100], password[100];
        int i;

        //以&为分隔符，前面的为用户名
        for (i = 5; m_string[i] != '&'; ++i)
            name[i - 5] = m_string[i];
        name[i - 5] = '\0';

        //以&为分隔符，后面的是密码
        int j = 0;
        for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
            password[j] = m_string[i];
        password[j] = '\0';

        //同步线程登录校验
        if (*(pos + 1) == '3') {
            //如果是注册，先检测数据库中是否有重名的
            //没有重名的，进行增加数据
            char* sql_insert = (char*) malloc( sizeof( char ) * 200 );
            strcpy( sql_insert, "INSERT INTO user(username, passwd) VALUES(" );
            strcat( sql_insert, "'" );
            strcat( sql_insert, name );
            strcat( sql_insert, "', '" );
            strcat( sql_insert, password );
            strcat( sql_insert, "')" );

            if (users.find( name ) == users.end()) {

                m_lock.lock();
                int res = mysql_query( mysql, sql_insert );
                users.insert( std::pair<std::string, std::string>( name, password ));
                m_lock.unlock();

                if (!res)
                    strcpy( url, "/log.html" );
                else
                    strcpy( url, "/registerError.html" );
            } else
                strcpy( url, "/registerError.html" );
        }
            //如果是登录，直接判断
            //若浏览器端输入的用户名和密码在表中可以查找到，返回1，否则返回0
        else if (*(pos + 1) == '2') {
            if (users.find( name ) != users.end() && users[name] == password)
                strcpy( url, "/welcome.html" );
            else
                strcpy( url, "/logError.html" );
        }



        //CGI多进程登录校验
    }

    // 如果请求资源为/0，表示跳转注册界面
    if (*(pos + 1) == '0') {
        char* m_url_real = (char*) malloc( sizeof( char ) * 200 );
        strcpy( m_url_real, "/register.html" );

        //将网站目录和/register.html进行拼接，更新到m_real_file中
        strncpy( realFile + len, m_url_real, strlen( m_url_real ));

        free( m_url_real );
    }
        // 如果请求资源为/1，表示跳转登录界面
    else if (*(pos + 1) == '1') {
        char* m_url_real = (char*) malloc( sizeof( char ) * 200 );
        strcpy( m_url_real, "/log.html" );

        //将网站目录和/log.html进行拼接，更新到m_real_file中
        strncpy( realFile + len, m_url_real, strlen( m_url_real ));

        free( m_url_real );
    }
        // 如果以上均不符合，即不是登录和注册，直接将url与网站目录拼接
        // 这里的情况是welcome界面，请求服务器上的一个图片
    else { strncpy( realFile + len, url, FILE_NAME_LEN - len - 1 ); }


    // 通过stat获取请求资源文件信息，成功则将信息更新到m_file_stat结构体
    // 失败返回NO_RESOURCE状态，表示资源不存在
    if (stat( realFile, &fileStat ) < 0) {
        // 若资源页不存在则HTTP解析结果为404
        return HttpCode::NO_RESOURCE;
    }
    // 判断文件的权限，是否可读，不可读则返回FORBIDDEN_REQUEST状态
    if (!(fileStat.st_mode & S_IROTH)) {
        // 资源没有权限获取
        return HttpCode::FORBIDDEN_REQUEST;
    }
    // 判断文件类型，如果是目录，则返回BAD_REQUEST，表示请求报文有误
    if (S_ISDIR( fileStat.st_mode )) {
        // 如果请求为目录, 请求就是错误的。
        return HttpCode::BAD_REQUEST;
    }
    // 以只读方式获取文件描述符，通过mmap将该文件映射到内存中
    int fd = open( realFile, O_RDONLY );
    // 将资源页文件映射到内存
    fileAddress = (char*) mmap( nullptr, fileStat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
    // 关掉文件描述, 避免文件描述符的浪费和占用
    close( fd );
    // 资源页请求成功
    // 表示请求文件存在，且可以访问
    return HttpCode::FILE_REQUEST;
}


/**
 * @brief 根据服务器处理HTTP请求的结果, 决定返回给客户端的内容。
 * <p>
 * 根据do_request的返回状态，服务器子线程调用process_write向m_write_buf中写入响应报文。
 * <p>
 * add_status_line函数，添加响应报文的状态行：http/1.1 状态码 状态消息
 * <p>
 * add_headers函数添加响应报文的报文头部，内部调用add_content_length和add_linger函数
 * <p>
 * content-length记录响应报文长度，用于浏览器端判断服务器是否发送完数据
 * <p>
 * connection记录连接状态，用于告诉浏览器端保持长连接
 * <p>
 * add_blank_line添加空行
 *
 * <p>
 * <p>
 * <p>
 * <p>
 * 响应报文分为两种，一种是请求文件的存在，通过io向量机制iovec，声明两个iovec，第一个指向m_write_buf，第二个指向mmap的地址m_file_address；
 * 一种是请求出错，这时候只申请一个iovec，指向m_write_buf。
 * @param code
 * @return
 */
bool HttpConnect::processWrite(HttpCode code) {
    switch (code) {
        // 服务器内部错误，500
        case HttpCode::INTERNAL_ERROR: {
            addStatusLine( 500, ERROR_500_TITLE );
            addHeaders( strlen( ERROR_500_FORM ));
            if (!addContent( ERROR_500_FORM )) {
                return false;
            }
            break;
        }
            // 报文语法有误，404
        case HttpCode::BAD_REQUEST: {
            addStatusLine( 400, ERROR_400_TITLE );
            addHeaders( strlen( ERROR_400_FORM ));
            if (!addContent( ERROR_400_FORM )) {
                return false;
            }
            break;
        }

        case HttpCode::NO_RESOURCE: {
            addStatusLine( 404, ERROR_404_TITLE );
            addHeaders( strlen( ERROR_404_FORM ));
            if (!addContent( ERROR_404_FORM )) {
                return false;
            }
            break;
        }
            // 资源没有访问权限，403
        case HttpCode::FORBIDDEN_REQUEST: {
            addStatusLine( 403, ERROR_403_TITLE );
            addHeaders( strlen( ERROR_403_FORM ));
            if (!addContent( ERROR_403_FORM )) {
                return false;
            }
            break;
        }
            // http请求报文的文件存在，200
            /*
             * 响应报文分为两种:
             * 一种是请求文件的存在，通过io向量机制iovec，声明两个iovec，第一个指向m_write_buf，第二个指向mmap的地址m_file_address；
             * 一种是请求出错，这时候只申请一个iovec，指向m_write_buf。
             *
             * iovec是一个结构体，里面有两个元素，指针成员iov_base指向一个缓冲区，这个缓冲区是存放的是writev将要发送的数据。
             * 成员iov_len表示实际写入的长度
             */
        case HttpCode::FILE_REQUEST: {
            addStatusLine( 200, OK_200_TITLE );
            // 如果请求文件存在
            if (fileStat.st_size != 0) {
                addHeaders( fileStat.st_size );
                // 第一个iovec指针指向响应报文缓冲区，长度指向m_write_idx
                ioVector[0].iov_base = writeBuf;
                ioVector[0].iov_len = writeIdx;
                // 第二个iovec指针指向mmap返回的文件指针，长度指向文件大小
                ioVector[1].iov_base = fileAddress;
                ioVector[1].iov_len = fileStat.st_size;
                ioVectorCount = 2;

                // 发送的全部数据为响应报文头部信息和文件大小
                bytesToSend = writeIdx + fileStat.st_size;

                return true;
            }
                // 如果请求的资源大小为0，则返回空白html文件
            else {
                const char* okStr = "<html><body></body></html>";
                addHeaders( strlen( okStr ));
                if (!addContent( okStr )) {
                    return false;
                }
            }
        }
        default: {
            return false;
        }
    }

    // 除FILE_REQUEST状态外，其余状态只申请一个iovec，指向响应报文缓冲区
    ioVector[0].iov_base = writeBuf;
    ioVector[0].iov_len = writeIdx;
    ioVectorCount = 1;

    bytesToSend = writeIdx;

    return true;
}

/**
 * @brief 添加响应报文的状态行
 * @param status
 * @param title
 * @return
 */
bool HttpConnect::addStatusLine(int status, const char* title) {
    return addResponse( "%s %d %s\r\n", "HTTP/1.1", status, title );
}

/**
 * @brief 往写缓冲中写入待发送的数据。
 * <p>
 * HTTP应答主要是将应答数据添加到写缓冲区writeBuf。
 * <p>
 * 此函数供其它函数内部调用更新m_write_idx指针和缓冲区m_write_buf中的内容。
 *
 * <p>
 * <p>
 * 这里可以后期改为 template variadic来处理。
 * @param format
 * @param ...
 * @return
 */
bool HttpConnect::addResponse(const char* format, ...) {
    // 如果写入内容超出m_write_buf大小则报错
    if (writeIdx >= WRITE_BUFFER_SIZE) {
        return false;
    }

    // 定义可变参数列表
    va_list argList;
    // 将变量arg_list初始化为传入参数
    va_start( argList, format );

    // 将format内容输出到writeBuf
    // 将数据format从可变参数列表写入缓冲区写，返回写入数据的长度
    // 重要！
    int len = vsnprintf( writeBuf + writeIdx, WRITE_BUFFER_SIZE - 1 - writeIdx, format, argList );
    // 如果写入的数据长度超过缓冲区剩余空间，则报错
    if (len >= (WRITE_BUFFER_SIZE - 1 - writeIdx)) {
        return false;
    }

    // 更新m_write_idx位置
    writeIdx += len;
    // 清空可变参列表
    va_end( argList );
    return true;
}

/**
 * @brief 添加响应报文的报文首部，具体添加文本长度、连接状态和空行
 * @param contentLength
 * @return
 */
bool HttpConnect::addHeaders(size_t contentLength) {
    // 添加响应报文的文本长度。添加Content-Length，表示响应报文的长度
    addContentLength( contentLength );
    // 添加响应报文的连接状态
    addLinger();
    // 添加空行
    addBlankLine();

//    return false;
    return true;
}

/**
 * @brief 添加Content-Length，表示响应报文的长度。
 * @param contentLength
 * @return
 */
bool HttpConnect::addContentLength(size_t contentLength) {
    return addResponse( "Content-Length: %d\r\n", contentLength );
}

/**
 * @brief 添加连接状态，通知浏览器端是保持连接还是关闭。
 * @return
 */
bool HttpConnect::addLinger() {
    return addResponse( "Connection: %s\r\n", linger ? "keep-alive" : "close" );
}

/**
 * @brief 添加空行。即 \r\n。
 * @return
 */
bool HttpConnect::addBlankLine() {
    return addResponse( "%s", "\r\n" );
}

/**
 * @brief 添加文本content
 * @param content
 * @return
 */
bool HttpConnect::addContent(const char* content) {
    return addResponse( "%s", content );
}

/**
 * @brief 对内存映射区执行 munmap() 操作。
 * <p>
 * 解除资源页文件映射的内存。
 */
void HttpConnect::unmap() {
    if (fileAddress) {
        //解除映射
        munmap( fileAddress, fileStat.st_size );
        fileAddress = nullptr;
    }
}


//////////////////////////////////////////////////////////////////////////////////////



void HttpConnect::initMysqlResult(DbConnectionPool* connPool) {
    //先从连接池中取一个连接
    MYSQL* mysql = NULL;
    DbConnectionRAII mysqlcon( &mysql, connPool );

    //在user表中检索username，passwd数据，浏览器端输入
    if (mysql_query( mysql, "SELECT username,passwd FROM user" )) {
//        LOG_ERROR( "SELECT error:%s\n", mysql_error( mysql ));
    }

    //从表中检索完整的结果集
    MYSQL_RES* result = mysql_store_result( mysql );

    //返回结果集中的列数
    int num_fields = mysql_num_fields( result );

    //返回所有字段结构的数组
    MYSQL_FIELD* fields = mysql_fetch_fields( result );

    //从结果集中获取下一行，将对应的用户名和密码，存入map中
    while (MYSQL_ROW row = mysql_fetch_row( result )) {
        std::string temp1( row[0] );
        std::string temp2( row[1] );
        users[temp1] = temp2;
    }
}